A dynamic run-time environment for a language such as JAVA™ is responsible for managing memory for objects that are created and destroyed during the execution of a program. An object is an entity that encapsulates data and, in some languages, operations associated with the object. Since the encapsulated data is stored in memory, objects are associated with particular regions of memory that are allocated and deallocated by the dynamic run-time environment.
The state of a program, or “program state,” is the set of the objects and the references between the objects that exist at a specific point in time during the execution of the program. A “reference” is used by a run-time environment to identify and ultimately access the region of memory for storing the data of the object. Typically, references between objects in a run-time environment are encoded using machine pointers. A machine pointer is an instance of a native type that contains the address of the object in the main memory, which can be a real memory address or, more commonly, a virtual address on a machine that implements a virtual memory system.
One popular run-time environment is a JAVA™ virtual machine, which supports a platform-independent, object-oriented language developed by Sun Microsystems. In JAVA, the attributes and methods for a class of objects are typically defined in a source file, which is compiled into an architecture-neutral object file containing bytecodes that are interpreted in the virtual machine at the target platform. It is common for objects to reference other objects.
Lately, there has been much interest in using JAVA in a multi-user environment that allows multiple users to connect in separate, concurrent sessions to a server system, such as a relational database system. When designing a run-time environment for such a multi-user environment, scalability in terms of the number of simultaneous users who can establish separate sessions is very important. A significant constraint for user scalability is the size of the memory footprint that each session consumes. For example, a server system may have 100 Mb of memory for supporting all the user sessions. If the session memory footprint is 1 Mb, then only 100 users can be supported at one time. Therefore, it is desirable to reduce the session memory footprint to improve scalability.
One approach for reducing the session memory footprint is to provide a shorter duration memory named “call memory” that is active for the duration of a “call” but automatically deallocated when the call terminates. A call is the period of time, when the user is actively using the server, such as during execution of a SQL statement in a database server. Accordingly, those objects that do not need to live beyond the duration of the call are placed in the call memory rather than session memory. When the call is completed, objects in the call memory are deallocated and the call-duration memory is reclaimed for us. This approach has been implemented in Oracle Corporation's PL/SQL language, for instance, in which objects are explicitly declared as having the duration of a call or of a session. Memory management in such a language is straightforward because the objects are simply allocated in the memory that corresponds to their duration.
JAVA, however, defines the lifetime of many objects, especially system objects, to extend throughout the duration of a session and does not have the notion of a call or call duration in the programming model. Therefore, the run-time environment mush have a policy for using call memory. Accordingly, one approach is to simply ignore the provision of the call memory by the multi-user system and allocate every object in session memory, but this approach suffers from scalability because short-lived objects are unnecessarily allocated in session memory.
Another approach is to allocate objects first in the shorter-duration call memory, and then, at the time the call terminates, migrate the objects into the longer duration session memory. In this approach, session memory is consumed only if the object is still alive at the end of the call. JAVA, however, imposes restrictions on this approach by requiring several system classes to store large objects in static class variables. Use of static class variables is also common in user code. In many cases, the state of the these objects is no longer relevant after the call, but they are still considered alive at the end of the call. An example of one of these objects is an I/O buffer in a stream class, which holds no useful information after being flushed but still consumes several kilobytes.
Thus, the above-described migration policy causes these large objects to be migrated into session memory even though the state of the large object is no longer relevant, thereby increasing session memory requirements per session and reducing scalability. The space used by such objects is substantial, on the order of 150 Kb for a JAVA “Hello World” program, thereby seriously affecting the scalability of the system. Therefore, there is a need for improving the scalability of such run-time environments.
To address this need, we have developed several techniques for reducing the session memory footprint. In some of the techniques, we deallocate a large object that is easily recreatable at the end of call. This deallocation prevents the large object from being migrated to session memory. We also arrange for that that large object to be recreated, for example in call memory, when needed in the next call. Consequently, we avoid consuming valuable session memory for the large object and improve the scalability of the run-time environment.
Some of these techniques are described in greater detail in the co-pending, commonly assigned applications, U.S. patent application Ser. No. 9/512,622 entitled METHOD FOR MANAGING MEMORY USING ACTIVATION-DRIVEN INITIALIZATION IN A RUN-TIME ENVIRONMENT, filed on Feb. 25, 2000 by Harlan Sexton et al. and U.S. patent application Ser. No. 09/512,619 entitled METHOD FOR MANAGING MEMORY USING EXPLICIT, LAZY INITALIZATION IN A RUN-TIME ENVIRONMENT, filed on Feb. 25, 2000 by Harlan Sexton et al., the contents of both of which are incorporated by reference in their entirety.
In order to use the above-described techniques, it is useful to affirmatively identify which objects are migrated into session memory. In a large run-time environment, such as a JAVA virtual machine, however, many objects are allocated and deallocated during the course of a call and it is difficult to identify, by manual inspection, those objects that would benefit most from applying such above-described techniques. Accordingly, there is a need for a diagnostic tool to identify these objects and, more generally, events of significance during execution of a program.
Conventional diagnostic tools, however, are unsatisfactory in addressing this need. For example, backtrace logging is form of performance monitoring that saves backtrace (i.e. a nested stack trace of the functions currently in effect) and symbol (e.g. the names of the functions) information obtained from a running program. The backtraces may be collected asynchronously, in response, for example, to an interrupt at short, uniform intervals, or synchronously, from calls to a system library. Normally, the backtraces are saved in a log file or files along with the symbol information used to interpret the backtraces. The logged information is then processed by another program, using the symbolic information, to merge the backtraces into a weighted tree and printed out. The following is an example of the kind of output produced by a backtrace logging tool:
1: —mainCRTStartup   98.84 2: —main   98.833: —ioe—external—test   97.534: —do—call   97.535: —ioe—execute   97.475: —ioct—access—IU   0.063: —ioe—init—call   1.304: —eoa—startup—default—objmems   1.285: —eoa—initialize—oldspace   1.215: —eoa—initialize—sessionspace   0.035: —eoa—initialize—runtime—space   0.015: —eoa—init—weak—objects   0.015: —eoa—initialize—stackspace   0.015: —eoa—initialize—unscanned—stackspace   0.01
In this output, it is seen that, for the data set the program had been using, the call pattern is that main( ) calls ioe—external—test( ) and ioe—init—call( ), with most of the time (97.53%) belonging to processing the ioe—external—test( ) or its callees.
Backtrace logging can also be used to produced a static image of the dynamic allocation patterns from executing the problem, by synchronously collecting the backtraces from the system allocation routines. Each of the stack traces in this case would be weighted by the number of bytes being allocated. The log file of conventional backtrace logging contains thousands of backtraces, making manual inspection for particular events very difficult. Moreover, the report generated by conventional backtrace logging aggregates the backtraces into a single chart that obliterates the contribution of a particular backtrace from that of other backtraces.
Therefore, there is a need for a diagnostic tool that can be used to identify the allocation of objects that are migrated as well as other significant events of interest.